import { BigNumber, utils, Wallet, providers, ContractFactory, Contract } from "ethers";
import * as path from "path";
import * as fs from "fs";

export const sleep = (s: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, s * 1000));

export class TestRunner {
    private provider: providers.JsonRpcProvider;
    private gasLimit = 21000;
    private gasPrice = BigNumber.from(10000000);
    private chainId = 1440001;
    private defaultValue = BigNumber.from(1000);
    private interface = new utils.Interface(["function test(uint _steps) public returns (uint)"]);
    private abi?: any;
    private bin?: any;

    constructor(url: string, private pKey: string, private address: string, private txAmount: number, private accAmount: number) {
        this.provider = new providers.JsonRpcProvider(url);
        this.updateGasPrice();
    }

    private async updateGasPrice() {
        try {
            this.gasPrice = await this.provider.getGasPrice();
        } catch (e) {
            console.log("Error fetching gas price");
        }
        await sleep(5);
        this.updateGasPrice();
    }

    private async signTransaction(wallet: Wallet, to: string, value: BigNumber, nonce?: number, mulPrice = 1): Promise<string> {
        if (nonce === undefined) {
            const transReq = await wallet.populateTransaction({
                to,
                gasPrice: this.gasPrice.mul(BigNumber.from(mulPrice)),
                gasLimit: this.gasLimit,
                value,
            });
            return wallet.signTransaction(transReq);
        }

        return await wallet.signTransaction({
            nonce,
            to,
            gasPrice: this.gasPrice.mul(BigNumber.from(mulPrice)),
            gasLimit: this.gasLimit,
            value,
            chainId: this.chainId,
        });
    }

    private getDeployNFTContractTransaction(wallet: Wallet, nonce: number): providers.TransactionRequest {
        const factory = new ContractFactory(this.abi, this.bin, wallet);
        const deployTransaction = factory.getDeployTransaction();
        deployTransaction.nonce = nonce;
        return deployTransaction;
    }

    public async sendManyTransactions(wallet: Wallet): Promise<void> {
        if (!wallet.provider) {
            wallet = wallet.connect(this.provider);
        }

        const transactions: string[] = [];

        for (let i = 0; i < this.txAmount; i++) {
            let mul = 1;
            const signAndSend = async () => {
                let tx;
                let walletNonce;
                try {
                    walletNonce = await wallet.getTransactionCount();
                    const p = await this.signTransaction(wallet, this.address, this.defaultValue, walletNonce, mul);
                    tx = await this.provider.sendTransaction(p);
                    await tx.wait(1);
                    return true;
                } catch (e) {
                    console.log(
                        `Failed tx ${tx?.hash} from ${
                            wallet.address
                        } with nonce ${walletNonce} and mul ${mul} and gasPrice ${this.gasPrice.toString()}`,
                    );
                    await sleep(Math.floor(Math.random() * 5));
                    mul++;
                    return false;
                }
            };
            let result = await signAndSend();
            while (!result) result = await signAndSend();
            // transactions.push(p);
        }

        // const promises: Promise<any>[] = [];
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        for (const transaction of transactions) {
            // await (await this.provider.sendTransaction(transaction)).wait();
            // promises.push((this.provider.sendTransaction(transaction)));
        }
        // await Promise.all(promises);
    }

    public async deployContracts(wallet: Wallet): Promise<void> {
        if (!wallet.provider) {
            wallet = wallet.connect(this.provider);
        }
        const walletNonce = await wallet.getTransactionCount();
        const transactions: providers.TransactionRequest[] = [];

        for (let i = 0; i < this.txAmount; i++) {
            transactions.push(this.getDeployNFTContractTransaction(wallet, i + walletNonce));
        }

        for (const transaction of transactions) {
            await wallet.sendTransaction(transaction);
        }
    }

    public async sendManyContractCalls(wallet: Wallet): Promise<void> {
        if (!wallet.provider) {
            wallet = wallet.connect(this.provider);
        }
        const walletNonce = await wallet.getTransactionCount();

        const transactions: providers.TransactionResponse[] = [];
        const deployTx = this.getDeployNFTContractTransaction(wallet, walletNonce);
        const resp = await (await wallet.sendTransaction(deployTx)).wait();

        const contract = new Contract(resp.contractAddress, this.interface, wallet);
        wallet.getTransactionCount();
        for (let i = 0; i < this.txAmount; i++) {
            const tx = await contract.test(10, { nonce: i + 1 + walletNonce });
            transactions.push(tx);
        }

        const promises: Promise<any>[] = [];
        for (const transaction of transactions) {
            await sleep(0.1);
            promises.push(transaction.wait());
        }

        await Promise.all(promises);
    }

    public async createAndFundAccounts(value: number): Promise<Wallet[]> {
        const funderAccount = new Wallet(this.pKey, this.provider);
        const accounts: Wallet[] = [];
        const nonce = await this.provider.getTransactionCount(funderAccount.address);
        for (let i = 0; i < this.accAmount; i++) {
            await sleep(0.2);
            const random = Wallet.createRandom();
            const account = new Wallet(random.privateKey, this.provider);
            accounts.push(account);
            const signedT = await this.signTransaction(funderAccount, account.address, utils.parseEther(value.toString()), nonce + i);
            await this.send(signedT);
            console.log(`"${account.privateKey}"${i === this.accAmount - 1 ? "" : ","}`);
        }

        return accounts;
    }

    public async send(signedT: any) {
        let done = false;
        while (!done) {
            try {
                await this.provider.sendTransaction(signedT);
                done = true;
            } catch (e) {
                done = false;
            }
        }
    }

    public async compile(): Promise<void> {
        const rawContract = fs.readFileSync(path.join(__dirname, "StressTester.sol"), "utf-8");
        const configuration = {
            language: "Solidity",
            sources: {
                "contract.sol": {
                    content: rawContract,
                },
            },
            settings: {
                outputSelection: {
                    "*": {
                        "*": ["*"],
                    },
                },
            },
        };

        let result: any;
        try {
            // eslint-disable-next-line @typescript-eslint/no-var-requires
            const solc = require("solc");
            result = JSON.parse(
                solc.compile(JSON.stringify(configuration), {
                    import: (dependency: string) => ({
                        contents: fs.readFileSync(require.resolve(dependency), "utf-8"),
                    }),
                }),
            );
        } catch (e: any) {
            console.log(Object.keys(e));
            console.log(e?.code);
            console.log(e.toString().substring(0, -100));
            // SHITTY ERROR THAT PRINTS ALL SOLC CONTENT ON CONSOLE.LOG
            throw Error("Solidity compile error");
        }

        const compiled = result.contracts["contract.sol"]["StressTester"];

        this.abi = compiled.abi;
        this.bin = compiled.evm.bytecode.object;
    }

    setUrl(url: string): void {
        this.provider = new providers.JsonRpcProvider(url);
    }

    setPKey(pKey: string): void {
        this.pKey = pKey;
    }

    setAddress(address: string): void {
        this.address = address;
    }

    setTxAmount(txAmount: number): void {
        this.txAmount = txAmount;
    }

    setAccAmount(accAmount: number): void {
        this.accAmount = accAmount;
    }

    async runTransactions(): Promise<void> {
        const accounts = await this.createAndFundAccounts(1);

        await Promise.all(accounts.map((account) => this.sendManyTransactions(account)));
    }

    async runContracts(): Promise<void> {
        if (!this.abi || !this.bin) {
            await this.compile();
        }
        const accounts = await this.createAndFundAccounts(100);

        await Promise.all(accounts.map((account) => this.sendManyContractCalls(account)));
    }
}
